package com.example.springbootdockertest.controller.pay;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.example.springbootdockertest.config.WxPayConfig;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Signature;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 微信支付测试
 *
 * @Author liguangcheng
 * @Date 2021/12/8 9:47 上午
 * @Vision 1.0
 **/
@RestController
@Slf4j
public class WXPayController {
    @Autowired
    private WxPayConfig wxPayConfig;
    @Autowired
    private CloseableHttpClient wxPayClient;
    @Autowired
    private Verifier verifier;
    private final ReentrantLock lock = new ReentrantLock();

    //微信小程序下单API，返回前端JSAPI调起支付API所需参数
    @RequestMapping("/jsApiPay")
    @ResponseBody
    public Map<String, String> jsApiPay() throws Exception {
        // 组装请求参数
        WXPayRequestParam wxPayParam = new WXPayRequestParam();
        wxPayParam.setMchid(wxPayConfig.getMchId());// 商户号
        wxPayParam.setAppid(wxPayConfig.getAppid());//appid
        wxPayParam.setNotify_url(wxPayConfig.getNotifyDomain().concat(WxEnum.JSAPI_NOTIFY.getType()));//回调地址
        wxPayParam.setOut_trade_no("订单号");
        wxPayParam.setDescription("商品描述");
        wxPayParam.setAttach("附加信息");
        WXPayRequestParam.Amount amount = new WXPayRequestParam.Amount();
        amount.setTotal(1);//订单总金额
        wxPayParam.setAmount(amount);
        WXPayRequestParam.Payer payer = new WXPayRequestParam.Payer();
        payer.setOpenid("openid");//用户的Openid
        wxPayParam.setPayer(payer);
        Gson gson = new Gson();
        String jsonParams = gson.toJson(wxPayParam);
        log.info("请求参数 ===> {}", jsonParams);
        //调用jsapi下单
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxEnum.JSAPI_PAY.getType()));
        StringEntity entity = new StringEntity(jsonParams, StandardCharsets.UTF_8);
        entity.setContentType(ContentType.JSON.toString());
        httpPost.setEntity(entity);
        httpPost.setHeader(Header.ACCEPT.toString(), ContentType.JSON.toString());
        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            //响应结果
            String bodyAsString = EntityUtils.toString(response.getEntity());
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            int statusCode = response.getStatusLine().getStatusCode();
            log.info("jsapi下单响应码 ={},返回结果 ={} ", statusCode, bodyAsString);
            if (!Objects.equals(HttpStatus.HTTP_OK, statusCode) && !Objects.equals(HttpStatus.HTTP_NO_CONTENT, statusCode)) {
                throw new RuntimeException(resultMap.get("message"));
            }
            //计算调用支付签名
            final String prepayId = resultMap.get("prepay_id");
            String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
            String packageStr = "prepay_id=" + prepayId;
            String nonceStr = this.getNonceStr();
            String message = buildSignMessage(wxPayConfig.getAppid(), timeStamp, nonceStr, packageStr);
            String paySign = this.sign(message.getBytes(), wxPayConfig.getPrivateKeyPath());
            //返回前端JSAPI调起支付API所需参数
            Map map = new HashMap() {{
                // put("prepayId", prepayId);
                put("package", packageStr);
                put("appId", wxPayConfig.getAppid());
                put("timeStamp", timeStamp);
                put("nonceStr", nonceStr);
                put("paySign", paySign);
            }};
            //新增支付日志

            return map;
        } catch (Exception e) {
            log.error("微信支付出错==>{}", e.getMessage());
        } finally {
            response.close();
        }
        return null;
    }

    //生产随机字符串
    public String getNonceStr() {
        StrBuilder strBuilder = StrBuilder.create();
        String now = DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN);
        strBuilder.append(now);
        Random random = new Random();
        for (int i = 0; i < 3; i++) {
            strBuilder.append(random.nextInt(10));
        }
        return strBuilder.toString();
    }

    /**
     * 构造签名串
     *
     * @param appid      小程序appId
     * @param timeStamp  时间戳
     * @param nonceStr   随机字符串
     * @param packageStr 订单详情扩展字符串
     * @return java.lang.String
     */
    private String buildSignMessage(String appid, String timeStamp, String nonceStr, String packageStr) {
        ArrayList<String> list = new ArrayList() {{
            add(appid);
            add(timeStamp);
            add(nonceStr);
            add(packageStr);
        }};
        StringBuilder sbf = new StringBuilder();
        for (String str : list) {
            sbf.append(str).append("\n");
        }
        return sbf.toString();
    }

    /**
     * 生成base64位签名信息
     *
     * @param message            请求体
     * @param privateKeyFilePath 私钥的路径
     */
    public String sign(byte[] message, String privateKeyFilePath) throws Exception {
        Signature sign = Signature.getInstance(SignAlgorithm.SHA256withRSA.toString());
        sign.initSign(PemUtil.loadPrivateKey(new FileInputStream(privateKeyFilePath)));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }


    //支付通知
    @PostMapping("/jsapi/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response) {
        //微信支付通过支付通知接口将用户支付成功消息通知给商户
        Gson gson = new Gson();
        Map<String, String> map = new HashMap<>();//应答对象
        try {
            //处理通知参数
            String body = this.readData(request);
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String) bodyMap.get("id");
            log.info("支付通知的id ===> {}", requestId);
            log.info("支付通知的完整数据 ===> {}", body);
            //int a = 9 / 0;
            //签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
                    = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if (!wechatPay2ValidatorForRequest.validate(request)) {
                log.error("通知验签失败");
                //失败应答
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "通知验签失败");
                return gson.toJson(map);
            }
            log.info("通知验签成功");
            //处理支付后交易状态和运单状态
            this.processState(bodyMap);
            //应答超时
            //模拟接收微信端的重复通知
            // TimeUnit.SECONDS.sleep(5);
            //成功应答
            response.setStatus(200);
            map.put("code", "SUCCESS");
            map.put("message", "成功");
            return gson.toJson(map);
        } catch (Exception e) {
            e.printStackTrace();
            //失败应答
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "失败");
            return gson.toJson(map);
        }

    }

    /**
     * 将通知参数转化为字符串
     *
     * @param request
     * @return
     */
    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //处理支付后交易状态和运单状态
    public void processState(Map<String, Object> bodyMap) throws Exception {
        //解密报文
        String plainText = decryptFromResource(bodyMap);
        //将明文转换成map
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String) plainTextMap.get("out_trade_no");
        //在对业务数据进行状态检查和处理之前，要采用数据锁进行并发控制， 以避免函数重入造成的数据混乱
        //尝试获取锁： 成功获取则立即返回true，获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()) {
            try {
                //处理重复的通知 接口调用的幂等性：无论接口被调用多少次，产生的结果是一致的。
                //通过订单号查询支付状态 已支付就直接返回

                // List<PaymentInfo> paymentInfos = paymentInfoService.getPayStatusByOrderNo(orderNo);
                // List<String> tradeStates = paymentInfos.stream().map(PaymentInfo::getTradeState).collect(Collectors.toList());
                // if (tradeStates.contains(WxEnum.SUCCESS.getType())) {
                //     return;// 已支付
                // }

                //模拟通知并发
                // try {
                // 	TimeUnit.SECONDS.sleep(5);
                // } catch (InterruptedException e) {
                // 	e.printStackTrace();
                // }

                //更新支付状态
                // paymentInfoService.updatePaymentInfo(plainText, plainTextMap, paymentInfos);

                //处理运单状态
                // this.dealManifestState(plainTextMap, paymentInfos);

            } catch (Exception e) {
                e.printStackTrace();
                log.error("支付通知错误,交易号==>{}", orderNo);
            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }
    }

    //对称解密
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("密文解密");
        //通知数据
        Map<String, String> resourceMap = (Map) bodyMap.get("resource");
        //数据密文
        String ciphertext = resourceMap.get("ciphertext");
        //随机串
        String nonce = resourceMap.get("nonce");
        //附加数据
        String associatedData = resourceMap.get("associated_data");
        log.info("密文 ===> {}", ciphertext);
        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);
        log.info("明文 ===> {}", plainText);
        return plainText;
    }


    // 微信小程序服务通知推送单个用户
    public String pushOneUser() {
        //获取access_token
        String access_token = this.getAccess_token(wxPayConfig.getAppid(), wxPayConfig.getSecret());
        ////拼接推送的模版
        WxMssDTO wxMssVo = new WxMssDTO();
        wxMssVo.setTouser("wxOpenid");//用户openid
        wxMssVo.setTemplate_id(wxPayConfig.getTemplateId());//模版id
        //跳转小程序类型：developer为开发版；trial为体验版；formal为正式版；默认为正式版
        wxMssVo.setMiniprogram_state("trial");
        wxMssVo.setPage("pages/order/order?foo=bar");
        Map<String, WxMssDTO.TemplateData> dataMap = new HashMap<>(5);
        
        //模板内容
        //订单单号{{character_string1.DATA}}
        // 订单金额{{amount3.DATA}}
        // 计费重量{{thing5.DATA}}
        // 待付金额{{amount11.DATA}}
        // 提醒内容{{thing2.DATA}}

        //订单单号{{character_string1.DATA}}
        WxMssDTO.TemplateData keyword1 = new WxMssDTO.TemplateData();
        keyword1.setValue("订单单号");
        dataMap.put("character_string1", keyword1);
        // 订单金额{{amount3.DATA}}
        WxMssDTO.TemplateData keyword2 = new WxMssDTO.TemplateData();
        keyword2.setValue("订单金额");
        dataMap.put("amount3", keyword2);
        wxMssVo.setData(dataMap);
        // 计费重量{{thing5.DATA}}
        WxMssDTO.TemplateData keyword3 = new WxMssDTO.TemplateData();
        keyword3.setValue("计费重量");
        dataMap.put("thing5", keyword3);
        wxMssVo.setData(dataMap);
        // 待付金额{{amount11.DATA}}
        WxMssDTO.TemplateData keyword4 = new WxMssDTO.TemplateData();
        keyword4.setValue("待付金额");
        dataMap.put("amount11", keyword4);
        wxMssVo.setData(dataMap);
        // 提醒内容{{thing2.DATA}}
        WxMssDTO.TemplateData keyword5 = new WxMssDTO.TemplateData();
        keyword5.setValue("【KEC】您共有1笔订单待支付运费");
        dataMap.put("thing2", keyword5);
        wxMssVo.setData(dataMap);

        String messageUrl = wxPayConfig.getMessageUrl().concat(access_token);
        String body = JSONUtil.toJsonStr(wxMssVo);
        String result = HttpUtil.post(messageUrl, body);
        log.info("小程序推送url=>{},body=>{},result=>{}", messageUrl, body, result);
        return result;
    }

    /**
     * 获取access_token
     *
     * @param appid
     * @param appsecret
     * @return java.lang.String
     */
    public String getAccess_token(String appid, String appsecret) {
        //获取access_token
        String accessTokenUrl = StrUtil.format(wxPayConfig.getAccessTokenUrl(), appid, appsecret);
        String s = HttpUtil.get(accessTokenUrl);
        Map<String, String> resultMap = new Gson().fromJson(s, HashMap.class);
        String access_token = resultMap.get("access_token");
        return access_token;
    }
}
